using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace CodeDom
{
  internal abstract class TemplateTypeGenerator
  {
    private readonly CodeTypeReference[] _baseTypes;
    protected readonly TemplateNameGenerator _nameGenerator;
    protected Template _template;
    private string _name;
    private IList<TemplateFieldGenerator> _templateFields;

    protected TemplateTypeGenerator(Template template, CodeTypeReference[] baseTypes, TemplateNameGenerator nameGenerator)
    {
      _template = template;
      _baseTypes = baseTypes;
      _nameGenerator = nameGenerator;
    }

    protected string Name
    {
      get
      {
        if (String.IsNullOrEmpty(_name))
          _name = GenerateTypeName();
        return _name;
      }
    }

    public IEnumerable<TemplateFieldGenerator> TemplateFields
    {
      get
      {
        if (_templateFields == null)
        {
          _templateFields = new List<TemplateFieldGenerator>();
          foreach (TemplateField field in _template.GetTemplateFields(true, false).OrderBy(tf => tf.Name))
          {
            TemplateFieldGenerator fieldGenerator = FieldPropertyGeneratorFactory.Create(field);
            _templateFields.Add(fieldGenerator);
          }
        }
        return _templateFields;
      }
    }

    protected abstract string GenerateTypeName();

    public abstract CodeTypeDeclaration Generate();

    protected void AddRegionDirectives(CodeTypeMember startMember, CodeTypeMember endMember, string text)
    {
      var regionStart = new CodeRegionDirective(CodeRegionMode.Start, text);
      startMember.StartDirectives.Add(regionStart);
      var regionEnd = new CodeRegionDirective(CodeRegionMode.End, text);
      endMember.EndDirectives.Add(regionEnd);
    }

    protected CodeMethodInvokeExpression CreateAssertArgumentNotNullMethod(string argumentName)
    {
      return new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("Assert"), "ArgumentNotNull",
                                            new CodeArgumentReferenceExpression(argumentName),
                                            new CodePrimitiveExpression(argumentName));
    }

    protected virtual CodeMemberField GenerateField(TemplateFieldGenerator field)
    {
      CodeMemberField memberField = new CodeMemberField();
      memberField.Name = _nameGenerator.GetFieldName(field.TemplateField, Name);
      memberField.Type = field.GetFieldType();
      memberField.Attributes = MemberAttributes.Private;
      return memberField;
    }

    protected void AddProperties(CodeTypeDeclaration typeDeclaration)
    {
      foreach (TemplateFieldGenerator field in TemplateFields)
      {
        CodeMemberProperty property = GenerateProperty(field);
        typeDeclaration.Members.Add(property);
      }

      AddRegionDirectives<CodeMemberProperty>(typeDeclaration, "Properties");
    }

    private void AddRegionDirectives<T>(CodeTypeDeclaration typeDeclaration, string regionName) where T : CodeTypeMember
    {
      IEnumerable<T> properties = typeDeclaration.Members.OfType<T>();
      if (properties.Any())
      {
        AddRegionDirectives(properties.First(), properties.Last(), regionName);
      }
    }

    protected virtual CodeMemberProperty GenerateProperty(TemplateFieldGenerator field)
    {
      CodeMemberProperty property = new CodeMemberProperty
                                      {
                                        Name = _nameGenerator.GetPropertyName(field.TemplateField, Name),
                                        Type = field.GetFieldType(),
                                        Attributes = MemberAttributes.Public,
                                        HasGet = true,
                                        HasSet = true
                                      };

      const string comment = "<summary>\r\n" +
                             " Gets the value of the <c>{0}</c> field on the {1} template.\r\n" +
                             " </summary>\r\n" +
                             " <value>The value of the <c>{0}</c> field</value>";
      property.Comments.Add(new CodeCommentStatement(string.Format(comment, field.TemplateField.Name, field.TemplateField.Owner.Name), true));
      return property;
    }

    protected virtual CodeTypeDeclaration GenerateTypeDeclaration(TypeAttributes typeAttributes)
    {
      CodeTypeDeclaration typeDeclaration = new CodeTypeDeclaration(Name)
                                 {
                                   TypeAttributes = typeAttributes, 
                                   IsPartial = true
                                 };
      if (_baseTypes.Any())
        typeDeclaration.BaseTypes.AddRange(_baseTypes);
      typeDeclaration.Comments.Add(
        new CodeCommentStatement(
          string.Format("<summary>\r\n Class for Sitecore template {0}\r\n </summary>", _template.Path), true));

      return typeDeclaration;
    }

    protected void AddFields(CodeTypeDeclaration declaration)
    {
      var itemField = new CodeMemberField("Item", "_item");
      itemField.Attributes = MemberAttributes.Private;

      declaration.Members.Add(itemField);

      foreach (TemplateFieldGenerator field in TemplateFields)
      {
        CodeMemberField memberField = GenerateField(field);

        declaration.Members.Add(memberField);
      }

      IEnumerable<CodeMemberField> fields = declaration.Members.OfType<CodeMemberField>();
      if (fields.Any())
      {
        AddRegionDirectives(fields.First(), fields.Last(), "Fields");
      }
    }

    protected void AddConstructor(CodeTypeDeclaration declaration)
    {
      CodeConstructor constructor = new CodeConstructor();
      constructor.Attributes = MemberAttributes.Public;
      constructor.Parameters.Add(new CodeParameterDeclarationExpression("Item", "item"));

      CodeMethodInvokeExpression assertMethodCall = CreateAssertArgumentNotNullMethod("item");
      constructor.Statements.Add(assertMethodCall);

      var itemAssignment =
        new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_item"),
                                new CodeVariableReferenceExpression("item"));
      constructor.Statements.Add(itemAssignment);

      declaration.Members.Add(constructor);

      string comment = "<summary>\r\n" +
                       " Initializes a new instance of the <see cref=\"{0}\"/> class.\r\n" +
                       " </summary>\r\n" +
                       " <param name=\"item\">\r\n The item to wrap. The item must derive from the <c>{1}</c> template, otherwise an exception will be thrown.\r\n </param>";
      constructor.Comments.Add(new CodeCommentStatement(string.Format(comment, declaration.Name, _template.Name),
                                                        true));

      AddRegionDirectives(constructor, constructor, "Constructor");
    }

    protected void AddMethods(CodeTypeDeclaration declaration)
    {
      foreach (TemplateFieldGenerator field in TemplateFields)
      {
        CodeExpression itemField = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_item");
        string methodName = _nameGenerator.GetFieldMethodName(field.TemplateField, Name);
        CodeMemberMethod method = new CodeMemberMethod();
        method.Name = methodName;
        method.ReturnType = field.GetFieldType();
        method.Attributes = MemberAttributes.Private;
        field.AddValueConversionStatements(itemField, method);
        
        declaration.Members.Add(method);
      }

    }
  }
}